/*
 * MainGame.java
 * 
 * Created on 2008-04-16, 12:27:12
 */

package planesimulator.game;

import java.util.logging.Logger;
import java.util.logging.Level;

import com.jme.app.BaseGame;
import com.jme.input.ChaseCamera;
import com.jme.input.InputHandler;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.light.DirectionalLight;
import com.jme.light.PointLight;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.renderer.pass.BasicPassManager;
import com.jme.renderer.pass.RenderPass;
import com.jme.renderer.pass.ShadowedRenderPass;
import com.jme.scene.CameraNode;
import com.jme.scene.Node;
import com.jme.scene.SceneElement;
import com.jme.scene.Text;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.CullState;
import com.jme.scene.state.FogState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.RenderState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.system.JmeException;
import com.jme.util.Timer;

import com.jmex.effects.water.WaterRenderPass;
import com.jmex.terrain.TerrainPage;

import java.nio.FloatBuffer;

import planesimulator.plane.AbstractPlane;
import planesimulator.plane.CessnaCitation;
import planesimulator.terrain.LandscapeBuilder;
import planesimulator.utils.SkyBoxContainer;
import planesimulator.utils.skybox.SkyBoxAdder;

/**
 * Main class of Simulator game.
 * 
 * @author Mateusz 'm4linka' Malinowski (m4linka@gmail.com)
 */
public class MainGame extends BaseGame {
  
  /** Logger */
  private static Logger logger = 
      Logger.getLogger(MainGame.class.getName());
  
  /** Initial image */
  private static String INITIAL_IMAGE =
      "planesimulator/data/textures/clouds.png";
  
  /** Definitions of near and far planes of frustum perspective */
  private static float NEAR_PLANE = 1.0f;
  private static float FAR_PLANE = 7000.0f;
  
  /** Instance of this class */
  private static MainGame INSTANCE = new MainGame();
  
  /** 
   * Definition of viewing angle 
   * (neccesarry in frustum perspective's definition) 
  */
  private static float VIEW_ANGLE = 45.0f;
  
  /** Exit command */
  private static String EXIT_COMMAND = "exit";
  
  /** Number of weathers */
  private static int NUM_WEATHERS = 1;
  
  /** The weather (skybox's name) */
  private static String WEATHERS[] = new String[NUM_WEATHERS];
  
  /** True if the game is in the test mode */
  private static boolean test_mode = true;
  
  /** True if we are in god mode */
  private static boolean god_mode = false;
  
  /** The current weather pointer */
  private static int weather_pointer = 0;
  
  private static int WEATHER_INTERVALS = 10000;
  
  /** Point out what actual weather there is (determines the skybox) */
  private static String current_weather;
  
  /** The root of scene graph */
  private static Node scene_node;
  
  /** Terrain */
  private static TerrainPage terrain;
  
  /** The light state */
  private static LightState light_state;
  
  /** The player */
  private static Player player;
  
  private static ShadowedRenderPass shadow_pass = new ShadowedRenderPass();
  
  
  /** 
   * The timer;
   * We use it as the game synchronizer
   */
  private static Timer timer;
  
  /** 
   * The sky timer;
   * In the beginning it is set up to 0, 
   * during the game it points out the time in the game;
   * Thanks to it the game can control the daycycle
   * (and switch from one skybox to the another)
   */
  private static Timer sky_timer;
  
  /** The main (cockpit) camera */
  private static Camera cockpit_camera;
  
  /** The chaising camera */
  private static Camera  chase_camera;

  /** Current camera */
  private static Camera camera;
  
  /** Boolean depending on camera setting **/
  private boolean is_chase_camera = false;
  
  /** The chaser camera */
  private static ChaseCamera chaser;
  
  /** The input handler */
  private static InputHandler input;
  
  /** The game input handler */
  private static InputHandler game_input;
  
  
  /** The skybox */
  private static SkyBoxContainer skybox_container;
  
  /** Some usefull display attributes such as screen width, height and depth */
  private static int width, height, depth;
  
  /** The display attribute, frequency of monitor */
  private static int freq;
  
  /** The display attribute, whether we are in fullscreen mode or not */
  private static boolean isFullscreen;
  
  /** The pass manager */
  private static BasicPassManager p_manager; 
  
  /** Quad of water */
  private static Quad waterQuad;
  
  /** Water render pass */
  private static WaterRenderPass water_effect_render_pass;
  
    /** FPS text */
  private static Text fps_text;
  
  /** FPS node */
  private static Node fps_node;
  
  /** Text that shows player's position */
  private static Text position_text;
  
  /** The position text node */
  private static Node position_node;
  
  /**
   * Constructor.
   * 
   */
  private MainGame() {
    super();
  }
  
  
  /**
   * 
   * @return this class' instance
   */
  public final static MainGame getInstance() {
    return INSTANCE;
  }
  
  
  /**
   * The entry point of the game.
   * 
   * @param args
   */
  public static void main(final String[] args) {
    final MainGame mainGame = MainGame.getInstance();
    mainGame.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG,
        Thread.currentThread().getContextClassLoader()
            .getResource(INITIAL_IMAGE));
//    new ShadowTweaker(shadow_pass).setVisible(true);
    mainGame.start();
  }
  
  
  /**
   * Initialize the display and the main camera.
   * 
   * @see com.jme.app.BaseGame#initSystem()
   */
  @Override
  protected void initSystem() {
    if(test_mode) {
      logger.info("TEST MODE");
    }
    
    // store the properties info
    width = properties.getWidth();
    height = properties.getHeight();
    depth = properties.getDepth();
    freq = properties.getFreq();
    isFullscreen = properties.getFullscreen();
    
    // set the renderer (display) system
    try {
      display = DisplaySystem.getDisplaySystem(properties.getRenderer());
      display.setMinStencilBits(8);
      display.createWindow(width, height, depth, freq, isFullscreen);
      cockpit_camera = display.getRenderer().createCamera(width, height);
      chase_camera = display.getRenderer().createCamera(width, height);
    } catch (JmeException e) {
      logger.log(Level.SEVERE, "Couldn't create display system", e);
      System.exit(1);
    }
    
    // set the background to black
    display.getRenderer().setBackgroundColor(ColorRGBA.black);
    
    // initialize the camera
    cockpit_camera.setFrustumPerspective(VIEW_ANGLE, (float)width / (float)height,
        NEAR_PLANE, FAR_PLANE);
     chase_camera.setFrustumPerspective(VIEW_ANGLE, (float)width / (float)height,
        NEAR_PLANE, FAR_PLANE);
    
    // signal that we've changed camera's frustum
    cockpit_camera.update();
    chase_camera.update();
    // get jme timer
    timer = Timer.getTimer();
    
    // set the camera
    display.getRenderer().setCamera(cockpit_camera);
    
    // bind the escape key
    KeyBindingManager.getKeyBindingManager().set(
        EXIT_COMMAND, KeyInput.KEY_ESCAPE);
  }
  
  
  /**
   * Initialize game's objects.
   */
  @Override
  protected void initGame() {
    display.setTitle("Plane Simulator");
    
    // create scene root graph
    scene_node = new Node("Scene graph node");
    
    // create z-buffer
    final ZBufferState z_buffer = display.getRenderer().createZBufferState();
    z_buffer.setEnabled(true);
    z_buffer.setFunction(ZBufferState.CF_LEQUAL);
    scene_node.setRenderState(z_buffer);
    
    // not render the back face triangles (optimalisation)
    final CullState cull_state = display.getRenderer().createCullState();
    cull_state.setCullMode(CullState.CS_BACK);
    scene_node.setRenderState(cull_state);
    
    // set camera
    setCamera(2);
    
    // add objects to the scene
    
    if(test_mode) {
      // add fps text
      buildFpsText();

      // add position text
      buildPositionText();
    }
    
    // add sky timer
    buildSkyTimer();

    // add the skybox
    buildSkyBox();
    
    // add water
    buildWater();
    
    // add terrain
    buildTerrain();
    
      // add player
    buildPlayer();
    
    // add chase camera
    buildChaseCamera();
    
    // add the input
    buildInput();
    
    // add fog
    buildFog();
    
    // add lights
    buildLights();
    
    buildShadows();
    
    buildPassManager();
    
    // update the scene graph
    scene_node.updateGeometricState(0.0f, true);
    scene_node.updateRenderState();
  }
  
  
  /**
   * Update the whole game (timer, inputs, scene, etc.).
   * 
   * @param interpolation not used as a parameter
   * @see com.jme.app.SimpleGame#update
   */
  @Override
  protected void update(final float interpolation) {
    // update the time to get the framerate
    timer.update();
    final float interpolate = timer.getTimePerFrame();
    
    // update the keyboard input 
    input.update(interpolate);
    game_input.update(interpolate);
    
    // update the chaser (chase camera) to handle the player around
    if (is_chase_camera) {
      chaser.update(interpolate);
    }
    
    // update skybox 
    skybox_container.getSkybox(current_weather)
        .setLocalTranslation(camera.getLocation());
    skybox_container.getSkybox(current_weather)
        .updateGeometricState(0.0f, true);
    
    // update player
    // player.getPlane().update(interpolate);
    player.update(interpolate);
    
    if(test_mode) {
      // update fps 
      fps_text.print("fps: " + (int)timer.getFrameRate());
      
      Vector3f position = player.getLocation();
      position_text.print("player's position [x = "  + (int)position.x +
          " y = " + (int)position.y + " z = " + (int)position.z + "]" +
          " terrain height: " + (int)terrain.getHeight(position) +
          " engine power (%): " + player.getEnginePower());
    }
    
    // if escape was pressed, we exit
    if(KeyBindingManager.getKeyBindingManager().isValidCommand(EXIT_COMMAND)) {
      finished = true;
    }
    
    // collision with terrain
    if(!god_mode) {
      // gets player's position
      Vector3f position = player.getLocation();
      
      if(position.y <= terrain.getHeight(position) + 50.0 || 
          position.y <= water_effect_render_pass.getWaterHeight() + 5) {
        // get crashed
        suicide();
      }
      
    }

    Vector3f trans_vector = new Vector3f(camera.getLocation().x,
        water_effect_render_pass.getWaterHeight(), camera.getLocation().z);
    
    setTextureCoords(0, trans_vector.x, -trans_vector.z, 0.02f);

    setVertexCoords(trans_vector.x, trans_vector.y, trans_vector.z);
    
    // update the scene graph
    scene_node.updateGeometricState(interpolate, true);
  }

  
  /**
   * Draws the scene graph.
   * 
   * @param interpolation not used
   * @see com.jme.app.SimpleGame#render
   */
  @Override
  protected void render(final float interpolation) {
    // clear the screen and draw the scene graph
    display.getRenderer().clearBuffers();
//    display.getRenderer().draw(scene_node);
    p_manager.renderPasses(display.getRenderer());
  }
  

  /**
   * will be called if the resolution changes.
   * 
   * @see com.jme.app.BaseGame#reinit()
   */
  @Override
  protected void reinit() {
    display.recreateWindow(width, height, depth, freq, isFullscreen);
  }

  
  /**
   * Cleans the messy up.
   * 
   * @see com.jme.app.BaseGame#cleanup()
   */
  @Override
  protected void cleanup() {
    water_effect_render_pass.cleanup();
    logger.info("Clean up.");
  }
  
  
  /**
   * Out of the game.
   */
  @Override
  protected void quit() {
    super.quit();
    logger.info("Quit invoked.");
    System.exit(0);
  }
  
  
  /**
   * Calls when we committed suicide.
   */
  protected void suicide() {
    logger.info("Suicide invoked");
    super.finish();
  }

  protected void setCamera(int cam_num){
    switch(cam_num){
      case (1): display.getRenderer().setCamera(chase_camera);
      is_chase_camera = true;
      camera = chase_camera;
      break;
      case (2):  display.getRenderer().setCamera(cockpit_camera); 
      is_chase_camera = false;
      camera = cockpit_camera;
      break;

    }
  }
  
  
  private void buildShadows() {
    shadow_pass.add(scene_node);
    shadow_pass.addOccluder(player.getPlane());
    shadow_pass.setRenderShadows(true);
    shadow_pass.setLightingMethod(ShadowedRenderPass.MODULATIVE);
  }
  
  
  /**
   * Must be invoked as last
   */
  private void buildPassManager() {
    scene_node.setRenderQueueMode(Renderer.QUEUE_OPAQUE);
    
    p_manager = new BasicPassManager();
    
    // Add skybox first to make sure it is in the background
    RenderPass render_pass = new RenderPass();
    render_pass.add(skybox_container.getSkybox(current_weather));
    p_manager.add(render_pass);
    p_manager.add(water_effect_render_pass);
    p_manager.add(shadow_pass);  
  }
  
  
  /** 
   * Builds fps text.
   */
  private void buildFpsText() {
    // sets our text
    fps_text = Text.createDefaultTextLabel("FPS label", "fps: 0");
    fps_text.setCullMode(SceneElement.CULL_NEVER);
    fps_text.setTextureCombineMode(TextureState.REPLACE);
    
    // sets our fps node
    fps_node = new Node("FPS node");
    fps_node.setRenderState(fps_text.getRenderState(RenderState.RS_ALPHA));
    fps_node.setRenderState(fps_text.getRenderState(RenderState.RS_TEXTURE));
    fps_node.attachChild(fps_text);
    scene_node.attachChild(fps_node);
  }
  
  /**
   * Builds position text
   */
  private void buildPositionText() {
    // sets our text
    position_text = Text.createDefaultTextLabel("POS label", "pos: 0");
    position_text.setCullMode(SceneElement.CULL_NEVER);
    position_text.setTextureCombineMode(TextureState.REPLACE);
    
    // sets our fps node
    position_node = new Node("Position text node");
    position_node.setLocalTranslation(0.0f, display.getHeight() - 20, 0.0f);
    position_node.setRenderState(position_text.getRenderState(
        RenderState.RS_ALPHA));
    position_node.setRenderState(position_text.getRenderState(
        RenderState.RS_TEXTURE));
    position_node.attachChild(position_text);
    scene_node.attachChild(position_node);
  }

  
  /**
   * Builds the sky timer.
   */
  private void buildSkyTimer() {
    sky_timer = Timer.getTimer();
    sky_timer.reset();
  }
  
  
  /**
   * Builds player.
   * 
   */
  private void buildPlayer() {
    // for now we use a simple box
    player = new Player();
    AbstractPlane plane = new CessnaCitation(terrain);
    plane.setLocalTranslation(new Vector3f(0.0f, 3500.0f, 0.0f));
    
    scene_node.attachChild(plane);
//    plane.attachChild(box);
    plane.updateWorldBound();
    player.setPlane(plane);

    scene_node.attachChild(plane);
    Box gear1 = new Box("Wheel 1",new Vector3f(0f, 0f, -3.0f),0.5f,0.5f,0.5f);
    Box gear2 = new Box("Wheel 1",new Vector3f(0f, -0f,  3.0f),0.5f,0.5f,0.5f);
    gear1.updateModelBound();
    gear2.updateModelBound();

    
    scene_node.attachChild(gear1);
    scene_node.attachChild(gear2);
    
    plane.attachChild(gear1);
    plane.attachChild(gear2);
    
    
    /* flight camera */
    CameraNode cn;
    cn = new CameraNode("camera", cockpit_camera);
    Quaternion start = new Quaternion();
    start.fromAngles(0f, (float) Math.PI / 2, 0f);
    cn.setLocalRotation(start);
    cn.setLocalTranslation(6.7f,0,0);
    plane.attachChild(cn);
    plane.updateWorldBound();
  }
    
  
  /**
   * Builds the chase camera.
   */
  private void buildChaseCamera() {
    // assiocate chaser with camera and player
    chaser = new ChaseCamera(chase_camera, player.getPlane());
    chaser.setMaxDistance(40.0f);
    chaser.setMinDistance(20.0f);
  }

  
  /**
   * Creates the game's input handler.
   */
  private void buildInput() {
    input = new PlaneInputHandler(
        (CessnaCitation)player.getPlane(), properties.getRenderer());
   game_input = new GameInputHandler(this);
  }
  
  
  /**
   * Builds the sky box.
   */
  private void buildSkyBox() {
    // reads weather's names 
//    for(int i = 0; i < NUM_WEATHERS; i++) {
//      WEATHERS[i] = "daytime/" + (i+1);
//      System.out.println(WEATHERS[i]);
//    }
    WEATHERS[0] = "daytime/9";
    
    skybox_container = new SkyBoxContainer(100.0f,100.0f,100.0f);
    for(String weather_name : WEATHERS) {
      SkyBoxAdder.addToContainer(skybox_container, weather_name);
    }
    current_weather = WEATHERS[0];
//    scene_node.attachChild(skybox_container.getSkybox(current_weather));
    logger.info("SkyBoxes loaded.");
  }
  
  /**
   * Build the fog.
   */
  private void buildFog() {
    final float fog_factor = 0.5f;
    
    FogState fogState = display.getRenderer().createFogState();
    fogState.setDensity(1.f);
    fogState.setEnabled(true);
    fogState.setColor(new ColorRGBA(0.9f, 0.9f, 0.9f, 1.0f));
    fogState.setEnd(FAR_PLANE);
    fogState.setStart(FAR_PLANE * fog_factor);
    fogState.setDensityFunction(FogState.DF_LINEAR);
    fogState.setApplyFunction(FogState.AF_PER_VERTEX);
    scene_node.setRenderState(fogState);
  }
        
  
  /** 
   * Builds terrain.
   */
  private void buildTerrain() {
    terrain = LandscapeBuilder.build(display);
    scene_node.attachChild(terrain);
    logger.info("Terrain have been builded.");
  }
  
  
  /**
   * Builds water.
   * 
   * NOTE: Call after build terrain and build skybox
   */
  private void buildWater() {
    water_effect_render_pass = new WaterRenderPass(cockpit_camera, 6, false, true);
    water_effect_render_pass.setWaterPlane(new com.jme.math.Plane(
        new Vector3f(0.0f, 1.0f,0.0f), 200.0f));
    water_effect_render_pass.setClipBias(-1.0f);
    water_effect_render_pass.setWaterMaxAmplitude(5.0f);
    water_effect_render_pass.setReflectionThrottle(0.0f);
    water_effect_render_pass.setRefractionThrottle(0.0f);

    waterQuad = new Quad("waterQuad", 1, 1);
    FloatBuffer normBuf = waterQuad.getNormalBuffer(0);
    normBuf.clear();
    normBuf.put(0).put(1).put(0);
    normBuf.put(0).put(1).put(0);
    normBuf.put(0).put(1).put(0);
    normBuf.put(0).put(1).put(0);

    water_effect_render_pass.setWaterEffectOnSpatial(waterQuad);
    scene_node.attachChild(waterQuad);
    
    water_effect_render_pass.setReflectedScene(
        skybox_container.getSkybox(current_weather));
    water_effect_render_pass.setSkybox(
        skybox_container.getSkybox(current_weather));
    water_effect_render_pass.setEnabled(true);

    logger.info("Water builded");
  }
  
  
  private void buildLights() {
    PointLight point_light = new PointLight();
    point_light.setEnabled(true);
    point_light.setDiffuse(new ColorRGBA(.2f, .2f, .23f, 1.0f));
    point_light.setAmbient(new ColorRGBA(.25f, .25f, .28f, .25f));
    point_light.setLocation(new Vector3f(20, 9000, 20));

    
    DirectionalLight dir_light = new DirectionalLight();
    dir_light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
    dir_light.setAmbient(new ColorRGBA(.5f, .5f, .5f, .5f));
    dir_light.setDirection(new Vector3f(1.0f,-1.0f,0.5f).normalizeLocal());
    dir_light.setShadowCaster(true);
    dir_light.setEnabled(true);
    
    /** Attach the light to a lightState and the lightState to rootNode. */
    light_state = display.getRenderer().createLightState();
    light_state.setEnabled(true);
    light_state.detachAll();
    light_state.attach(dir_light);
    light_state.attach(point_light);
    light_state.setGlobalAmbient(new ColorRGBA(0.6f, 0.6f, 0.6f, 1f));
    scene_node.setRenderState(light_state);
  }
  
  
  private void setVertexCoords(float x, float y, float z) {
    FloatBuffer vertBuf = waterQuad.getVertexBuffer(0);
    vertBuf.clear();

    vertBuf.put(x - FAR_PLANE).put(y).put(z - FAR_PLANE);
    vertBuf.put(x - FAR_PLANE).put(y).put(z + FAR_PLANE);
    vertBuf.put(x + FAR_PLANE).put(y).put(z + FAR_PLANE);
    vertBuf.put(x + FAR_PLANE).put(y).put(z - FAR_PLANE);
  }

  private void setTextureCoords(int buffer, float x, float y,
      float textureScale) {
    x *= textureScale * 0.5f;
    y *= textureScale * 0.5f;
    textureScale = FAR_PLANE * textureScale;
    FloatBuffer texBuf;
    texBuf = waterQuad.getTextureBuffer(0, buffer);
    texBuf.clear();
    texBuf.put(x).put(textureScale + y);
    texBuf.put(x).put(y);
    texBuf.put(textureScale + x).put(y);
    texBuf.put(textureScale + x).put(textureScale + y);
  }
  
  
  /**
   * Changes skybox.
   * 
   */
  private void switchSkyBox(final String weather_name) {
    scene_node.detachChild(skybox_container.getSkybox(current_weather));
    current_weather = weather_name;
    scene_node.attachChild(skybox_container.getSkybox(current_weather));
    scene_node.updateRenderState();
    logger.info("Sky box have been changed.");
  }
}
